package interfaz2;

import java.io.IOException;
import java.net.URL;
import java.util.List;

import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;

import core.Blog;
import core.Util;
import dustconf.DustConf;
import dustlog.LogController;
import events.DustMouseListener;
import events.UpdateCompleteEvent;
import events.UpdateCompleteListener;

/**
 * The view of the subscription option. Probably the main view of Dust, it
 * shows the Blogs we are subscribed to, the Posts of those Blogs, let us read
 * the Posts, add new Blogs, manage them, etc...
 * 
 * @author ole
 */
public class SubscriptionComposite extends DustComposite {
	// Class constants.
	/*
	 * These values will be use to positionate the sash between the tree of
	 * blogs and the browser that will show posts. You can set these values
	 * at the value you want but to make it more easy to handle try to make
	 * that their sum is 100 in order to treat them as a percentage (read
	 * SashForm documentation). 
	 */
	private final int TREE_WIDTH = 25;
	private final int BLOGSPOSTSCOMP_WIDTH = 75;
	
	// Class attributes.
	private Tree blogs_tree;
	private Browser browser;
	private SashForm sashform;
	private MultiViewComposite right_comp;
	private MVCBrowserComposite mvc_browser;
	private MultiViewComposite comp4posts;
	
	/*
	 * Private classes used with SubscriptionComposite class. These classes are
	 * mainly Listeners to handle events generated by the user.
	 */
	
	/**
	 * Class that will control the selection of the "Add Subscription" button.
	 */
	private class AddSubscriptionListener extends SelectionAdapter {
		/*
		 * Private classes.
		 */
		
		/**
		 * This class wraps all the work needed to subscribe to a new Blog.
		 * This is done this way (implementing Runnable) in order to not
		 * froze the UI.
		 * 
		 * author: ole
		 */
		private class NewSubscriptionTask implements Runnable {
			/**
			 * This class will be the handler executed when the update of the
			 * Blog just subscribed finish.
			 * 
			 * @author ole
			 */
			private class SubscriptionCompleteListener
			implements UpdateCompleteListener, Runnable {
				UpdateCompleteEvent event;
				
				@Override
				public void handleEvent(UpdateCompleteEvent event) {
					this.event = event;
					SubscriptionComposite.this.getDisplay().asyncExec(this);
				}

				@Override
				public void run() {
					Blog blog = event.getSource();
					
					// Log the action.
					LogController.log("Subscription to " + blog.getChannel() +
									  " completed.");
					
					// Save the blog so tomorrow when we run again Dust it
					// will be recovered.
					try {
						blog.save(DustConf.getSerializedBlogsPath());
					}
					catch (IOException e) {
						e.printStackTrace();
					}
					
					// Update the tree and the views.
					new BlogController(blog, blogs_tree, right_comp,
									   browser, comp4posts);
				}
			}
			
			@Override
			public void run() {
				String url_str, channel_str, url_channel_strs[];
				URL url;
				Blog blog;
				AddSubscriptionDialog dialog;
				SubscriptionCompleteListener callback;
				UpdateCompleteEvent event;
				Shell shell = getShell();
				
				// When the "Add subscription" button is selected we pop-up a
				// dialog to subscribe to a new blog that will return (if
				// success) a string array with the URL where the blog is and
				// the name of the channel.
				dialog = new AddSubscriptionDialog(shell);
				url_channel_strs = (String[])dialog.runDialog();
				if (url_channel_strs != null) {
					try {
						callback = new SubscriptionCompleteListener();
						
						// Subscribe to the new blog.
						url_str = url_channel_strs[0];
						channel_str = url_channel_strs[1];

						// Check if the url_str comes with http at the
						// beginning, if not try to use it as a file at local.
						if (url_str.startsWith("http")) {
							url = new URL(url_str);
							blog = new Blog(url, channel_str);
						}
						else {
							blog = new Blog(url_str, channel_str);
							event = new UpdateCompleteEvent(blog);
							callback.handleEvent(event);
						}	
					}
					catch (Exception e) {
						ExceptionBox ebox = new ExceptionBox(e, new Shell());
						ebox.popError();
					}
				}
			}
		}
		
		/*
		 * Public methods of class AddSubscriptionListener.
		 */

		@Override
		public void widgetSelected(SelectionEvent event) {
			NewSubscriptionTask nstask;
			
			nstask = new NewSubscriptionTask();
			getShell().getDisplay().asyncExec(nstask);
		}
	}
	
	/**
	 * Handles what to do when the tree of blogs is going to be disposed (for
	 * example when Dust is going to shut down).
	 * 
	 * @author ole
	 */
	private class BlogTreeDisposeListener implements DisposeListener {
		/**
		 * When the tree is going to be disposed first save all the blogs the
		 * user is subscribed to in order to make permanent the changes for the
		 * next time Dust is executed. As an example of change, when a post has
		 * been readed the next time Dust is run we don't want bold letters on
		 * it.
		 */
		@Override
		public void widgetDisposed(DisposeEvent event) {
			BlogController blog_item;

			try {
				for (TreeItem titem: blogs_tree.getItems()) {
					if (titem.getData() instanceof BlogController) {
						blog_item = (BlogController)titem.getData();
						blog_item.saveBlog(DustConf.getSerializedBlogsPath());
					}
				}
			}
			catch (Exception e) {
				ExceptionBox ebox;
				ebox = new ExceptionBox(e, new Shell());
				ebox.popError();
			}
		}
	}
	
	/**
	 * Class that will control MouseEvents with tree of Blogs.
	 * 
	 * @author ole
	 */
	private class BlogTreeMouseListener extends DustMouseListener {
		/*
		 * Private methods of BlogTreeMouseListener.
		 */
		
		/**
		 * Method that checks if the item is a Blog.
		 * 
		 * @return
		 * 		True if the Object is a Blog, false elsewhere.
		 */
		private boolean isBlogItem(Object titem) {
			if (titem != null && titem instanceof BlogController)
				return true;
			return false;
		}
		
		/**
		 * Method that checks if the item is a Post.
		 * 
		 * @return
		 * 		True if the Object is a Post, false elsewhere.
		 */
		private boolean isPostItem(Object titem) {
			if (titem != null && titem instanceof PostController)
				return true;
			return false;
		}
		
		/*
		 * Public methods of BlogTreeMouseListener.
		 */
		
		@Override
		public void mouseDoubleClick(MouseEvent event) {
			Tree tree;
			TreeItem titem;
			Point point;
			
			tree = (Tree)event.widget;
			point = new Point(event.x, event.y);
			titem = tree.getItem(point);
			
			// When double-clicking on the arrow near an item titem its null
			// a bit weird...
			if (titem == null)
				return;
			
			// Left double-clicking on a blog expands or contracts it.
			if (isLeftClick(event.button) && isBlogItem(titem.getData())) {
				if (titem.getExpanded())
					titem.setExpanded(false);
				else
					titem.setExpanded(true);
			}
		}

		@Override
		public void mouseUp(MouseEvent event) {
			Tree tree;
			TreeItem titem;
			Point point;
			BlogController blog_item;
			PostController post_item;
			int index;
			
			tree = (Tree)event.widget;
			point = new Point(event.x, event.y);
			titem = tree.getItem(point);
			
			// Ok, this is a bit tricky. When you click on the little arrow at
			// the left of a tree element (those element that have children)
			// titem is null, so trying to check if the data associated to it
			// is Blog or Post will throw a NullPointerException, so we control
			// this case. Why clicking on that arrow is not the same as
			// clicking on the element? Yes, I also asked why... I'm still
			// guessing...
			if (titem == null)
				return;
			
			// If right-clicking on a blog, show a menu with the options.
			if (isRightClick(event.button) && isBlogItem(titem.getData())) {
				blog_item = (BlogController)titem.getData();
				blog_item.showBlogMenu();
				return;
			}
			
			// If left-clicking on a post, show the post content inside the
			// browser, set it as read and set it with no bold letters.
			if (isLeftClick(event.button) && isPostItem(titem.getData())) {				
				blog_item = (BlogController)titem.getParentItem().getData();
				post_item = (PostController)titem.getData();
				
				// Discover the index inside the MVCBrowserComposite of the
				// PostView requested.
				index = mvc_browser.getMVC().getIndexOf(post_item.getView());
				
				// Show the requested PostView.
				mvc_browser.changeView(index);
				
				// Set the text normal (it may was bold) and mark the post as
				// read.
				post_item.setReadStyle(true);
				post_item.getPost().markAsRead();
				
				// Show this view corresponding to the PostViews and the
				// Browser.
				right_comp.changeView(0);
				return;
			}
			
			// If left-clicking on a blog, hide the last view inside the
			// Composite of the right sash (could be the MVCBrowserComposite or
			// BlogView) and show the BlogView requested.
			if (isLeftClick(event.button) && isBlogItem(titem.getData())) {
				blog_item = (BlogController)titem.getData();
				
				// The BlogView inside the MultiViewComposite is at the same
				// position that its node in the Tree + 1 because the first
				// element of the MultiViewComposite is the MVCBrowserComposite
				// that maintains the Browser and PostViews.
				right_comp.changeView(titem.getParent().indexOf(titem) + 1);
				return;
			}
			
			// If right-clicking on a Post, show a menu with the options.
			if (isRightClick(event.button) && isPostItem(titem.getData())) {
				post_item = (PostController)titem.getData();
				post_item.showPostMenu();
				return;
			}
		}
	}
	
	/*
	 * Public methods of SubscriptionComposite.
	 */
	
	/**
	 * Class constructor.
	 * 
	 * @throws Exception 
	 */
	public SubscriptionComposite(Composite parent, int style)
	throws Exception {
		super(parent, style);
		
		String images_path;
		List<Blog> blogs_list;
		
		images_path = Util.checkDirectoryPath(DustConf.getImagesFolder());
		
		// At the top we have a button to add new subscriptions.
		Button button = new Button(this, SWT.NONE);
		button.setText("Add subscription");
		button.setImage(IfaceUtils.buildImage(images_path + "rss_add.png"));
		GridData grid = new GridData(SWT.LEFT, SWT.TOP, false, false);
		button.setLayoutData(grid);
		
		// At the bottom we have a SashForm with two elements, a tree with
		// all the blogs we are subscribed to and a MultiViewComposite that
		// will show the BlogViews or PostViews.
		sashform = new SashForm(this, SWT.BORDER | SWT.HORIZONTAL);
		sashform.setLayout(new GridLayout(2, false));
		grid = new GridData(SWT.FILL, SWT.FILL, true, true);
		sashform.setLayoutData(grid);
		
		// Create the tree for blogs and posts.
		blogs_tree = new Tree(sashform, SWT.BORDER |
										SWT.H_SCROLL |
										SWT.V_SCROLL);
		grid = new GridData(SWT.LEFT, SWT.FILL, false, true);
		blogs_tree.setLayoutData(grid);
		
		// The right part of the sashform will show BlogViews or PostViews.
		right_comp = new MultiViewComposite(sashform, SWT.NONE);
		right_comp.setLayout(new GridLayout());
	    grid = new GridData(GridData.FILL, GridData.FILL, true, true);
	    right_comp.setLayoutData(grid);
	    
	    mvc_browser = new MVCBrowserComposite(right_comp, SWT.NONE);
	    grid = new GridData(GridData.FILL, GridData.FILL, true, true);
	    mvc_browser.setLayoutData(grid);
	    
	    browser = mvc_browser.getBrowser();
	    comp4posts = mvc_browser.getMVC();
	    
	    // Set the position of the sash between the tree and the
	    // BlogsPostsComposite.
		int sash_weights[] = {TREE_WIDTH, BLOGSPOSTSCOMP_WIDTH};
		sashform.setWeights(sash_weights);
		
	    // Recover the blogs we are already subscribed.
	    blogs_list = Util.getBlogs(DustConf.getSerializedBlogsPath());
	    
	    // Create a node in the tree for each blog, and a node for each post of
	    // that blog below its blog (this is done internally in the
	    // BlogController() method. Also add the BlogViews to the right_comp
	    // Composite.
	    for (Blog blog: blogs_list)
	    	new BlogController(blog, blogs_tree, right_comp,
	    					   browser, comp4posts);
	    
	    // Show the browser (empty) and no PostView.
	    right_comp.changeView(0);
		
		// Add Listeners.
		button.addSelectionListener(new AddSubscriptionListener());
		blogs_tree.addMouseListener(new BlogTreeMouseListener());
		blogs_tree.addDisposeListener(new BlogTreeDisposeListener());
	}
	
	/**
	 * Returns the Tree maintaining BlogControllers inside this Composite.
	 * 
	 * @return:
	 * 		The Tree that is maintaining the BlogControllers.
	 */
	public Tree getBlogsTree() {
		return blogs_tree;
	}
	
	/**
	 * Tells this Composite to stop any updating on the Blogs it is
	 * maintaining.
	 */
	public void terminateUpdate() {
		BlogController blog_ctrl;
		Object data_assoc;
		
		for (int i=0; i<blogs_tree.getChildren().length; i++) {
			data_assoc = blogs_tree.getChildren()[i].getData();
			if (data_assoc instanceof BlogController) {
				blog_ctrl = (BlogController)data_assoc;
				blog_ctrl.stopUpdate();
			}
		}
	}
}
